package org.inferred.freebuilder.processor.property;

import static org.inferred.freebuilder.processor.GeneratedTypeSubject.assertThat;
import static org.inferred.freebuilder.processor.model.ClassTypeImpl.INTEGER;
import static org.inferred.freebuilder.processor.model.ClassTypeImpl.STRING;
import static org.inferred.freebuilder.processor.model.PrimitiveTypeImpl.INT;
import static org.inferred.freebuilder.processor.model.TypeParameterElementImpl.newTypeParameterElement;
import static org.inferred.freebuilder.processor.source.FunctionalType.primitiveUnaryOperator;
import static org.inferred.freebuilder.processor.source.FunctionalType.unaryOperator;

import com.google.common.collect.ImmutableMap;

import org.inferred.freebuilder.processor.BuilderFactory;
import org.inferred.freebuilder.processor.Datatype;
import org.inferred.freebuilder.processor.GeneratedBuilder;
import org.inferred.freebuilder.processor.model.TypeParameterElementImpl;
import org.inferred.freebuilder.processor.source.QualifiedName;
import org.inferred.freebuilder.processor.source.feature.GuavaLibrary;
import org.junit.Test;

public class DefaultSourceTest {

  @Test
  public void twoPropertiesWithDefaults() {
    QualifiedName person = QualifiedName.of("com.example", "Person");
    QualifiedName generatedBuilder = QualifiedName.of("com.example", "Person_Builder");

    Datatype datatype = new Datatype.Builder()
        .setBuilder(person.nestedType("Builder").withParameters())
        .setExtensible(true)
        .setBuilderFactory(BuilderFactory.NO_ARGS_CONSTRUCTOR)
        .setBuilderSerializable(false)
        .setGeneratedBuilder(generatedBuilder.withParameters())
        .setHasToBuilderMethod(true)
        .setInterfaceType(false)
        .setPartialType(generatedBuilder.nestedType("Partial").withParameters())
        .setPropertyEnum(generatedBuilder.nestedType("Property").withParameters())
        .setType(person.withParameters())
        .setValueType(generatedBuilder.nestedType("Value").withParameters())
        .build();
    Property name = new Property.Builder()
        .setAllCapsName("NAME")
        .setBoxedType(STRING)
        .setCapitalizedName("Name")
        .setFullyCheckedCast(true)
        .setGetterName("name")
        .setName("name")
        .setType(STRING)
        .setUsingBeanConvention(false)
        .build();
    Property age = new Property.Builder()
        .setAllCapsName("AGE")
        .setBoxedType(INTEGER)
        .setCapitalizedName("Age")
        .setFullyCheckedCast(true)
        .setGetterName("age")
        .setName("age")
        .setType(INT)
        .setUsingBeanConvention(false)
        .build();
    GeneratedBuilder generatedType = new GeneratedBuilder(datatype, ImmutableMap.of(
        name, new DefaultProperty(datatype, name, true, unaryOperator(STRING)),
        age, new DefaultProperty(datatype, age, true, primitiveUnaryOperator(INT))));

    assertThat(generatedType).generates(
        "// Autogenerated code. Do not modify.",
        "package com.example;",
        "",
        "import com.example.Person;",
        "import java.util.Objects;",
        "import java.util.function.IntUnaryOperator;",
        "import java.util.function.UnaryOperator;",
        "",
        "/** Auto-generated superclass of {@link Person.Builder}, "
            + "derived from the API of {@link Person}. */",
        "abstract class Person_Builder {",
        "",
        "  /**",
        "   * Creates a new builder using {@code value} as a template.",
        "   *",
        "   * <p>If {@code value} is a partial, the builder will return more partials.",
        "   */",
        "  public static Person.Builder from(Person value) {",
        "    return value.toBuilder();",
        "  }",
        "",
        "  private String name;",
        "  private int age;",
        "",
        "  /**",
        "   * Sets the value to be returned by {@link Person#name()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code name} is null",
        "   */",
        "  public Person.Builder name(String name) {",
        "    this.name = Objects.requireNonNull(name);",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Replaces the value to be returned by {@link Person#name()} "
            + "by applying {@code mapper} to it and",
        "   * using the result.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code mapper} is null or returns null",
        "   */",
        "  public Person.Builder mapName(UnaryOperator<String> mapper) {",
        "    return name(mapper.apply(name()));",
        "  }",
        "",
        "  /** Returns the value that will be returned by {@link Person#name()}. */",
        "  public String name() {",
        "    return name;",
        "  }",
        "",
        "  /**",
        "   * Sets the value to be returned by {@link Person#age()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder age(int age) {",
        "    this.age = age;",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Replaces the value to be returned by {@link Person#age()} "
            + "by applying {@code mapper} to it and",
        "   * using the result.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code mapper} is null",
        "   */",
        "  public Person.Builder mapAge(IntUnaryOperator mapper) {",
        "    return age(mapper.applyAsInt(age()));",
        "  }",
        "",
        "  /** Returns the value that will be returned by {@link Person#age()}. */",
        "  public int age() {",
        "    return age;",
        "  }",
        "",
        "  /**",
        "   * Copies values from {@code value}, skipping defaults.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder mergeFrom(Person value) {",
        "    Person_Builder defaults = new Person.Builder();",
        "    if (!Objects.equals(value.name(), defaults.name())) {",
        "      name(value.name());",
        "    }",
        "    if (value.age() != defaults.age()) {",
        "      age(value.age());",
        "    }",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Copies values from {@code template}, skipping defaults.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder mergeFrom(Person.Builder template) {",
        "    Person_Builder defaults = new Person.Builder();",
        "    if (!Objects.equals(template.name(), defaults.name())) {",
        "      name(template.name());",
        "    }",
        "    if (template.age() != defaults.age()) {",
        "      age(template.age());",
        "    }",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Resets the state of this builder.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder clear() {",
        "    Person_Builder defaults = new Person.Builder();",
        "    name = defaults.name;",
        "    age = defaults.age;",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /** Returns a newly-created {@link Person} based on the contents of this "
            + "{@code Builder}. */",
        "  public Person build() {",
        "    return new Value(this);",
        "  }",
        "",
        "  /**",
        "   * Returns a newly-created partial {@link Person} for use in unit tests. "
            + "State checking will not",
        "   * be performed.",
        "   *",
        "   * <p>The builder returned by {@link Person.Builder#from(Person)} or "
            + "{@link Person#toBuilder()}",
        "   * will propagate the partial status of its input, overriding "
            + "{@link Person.Builder#build()",
        "   * build()} to return another partial. This allows for robust tests of modify-rebuild "
            + "code.",
        "   *",
        "   * <p>Partials should only ever be used in tests. "
            + "They permit writing robust test cases that won't",
        "   * fail if this type gains more application-level constraints "
            + "(e.g. new required fields) in",
        "   * future. If you require partially complete values in production code, "
            + "consider using a Builder.",
        "   */",
        "  public Person buildPartial() {",
        "    return new Partial(this);",
        "  }",
        "",
        "  private static final class Value extends Person {",
        "    private final String name;",
        "    private final int age;",
        "",
        "    private Value(Person_Builder builder) {",
        "      this.name = builder.name;",
        "      this.age = builder.age;",
        "    }",
        "",
        "    @Override",
        "    public String name() {",
        "      return name;",
        "    }",
        "",
        "    @Override",
        "    public int age() {",
        "      return age;",
        "    }",
        "",
        "    @Override",
        "    public Person.Builder toBuilder() {",
        "      Person_Builder builder = new Person.Builder();",
        "      builder.name = name;",
        "      builder.age = age;",
        "      return (Person.Builder) builder;",
        "    }",
        "",
        "    @Override",
        "    public boolean equals(Object obj) {",
        "      if (!(obj instanceof Value)) {",
        "        return false;",
        "      }",
        "      Value other = (Value) obj;",
        "      return Objects.equals(name, other.name) && age == other.age;",
        "    }",
        "",
        "    @Override",
        "    public int hashCode() {",
        "      return Objects.hash(name, age);",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      return \"Person{name=\" + name + \", age=\" + age + \"}\";",
        "    }",
        "  }",
        "",
        "  private static final class Partial extends Person {",
        "    private final String name;",
        "    private final int age;",
        "",
        "    Partial(Person_Builder builder) {",
        "      this.name = builder.name;",
        "      this.age = builder.age;",
        "    }",
        "",
        "    @Override",
        "    public String name() {",
        "      return name;",
        "    }",
        "",
        "    @Override",
        "    public int age() {",
        "      return age;",
        "    }",
        "",
        "    private static class PartialBuilder extends Person.Builder {",
        "      @Override",
        "      public Person build() {",
        "        return buildPartial();",
        "      }",
        "    }",
        "",
        "    @Override",
        "    public Person.Builder toBuilder() {",
        "      Person_Builder builder = new PartialBuilder();",
        "      builder.name = name;",
        "      builder.age = age;",
        "      return (Person.Builder) builder;",
        "    }",
        "",
        "    @Override",
        "    public boolean equals(Object obj) {",
        "      if (!(obj instanceof Partial)) {",
        "        return false;",
        "      }",
        "      Partial other = (Partial) obj;",
        "      return Objects.equals(name, other.name) && age == other.age;",
        "    }",
        "",
        "    @Override",
        "    public int hashCode() {",
        "      return Objects.hash(name, age);",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      return \"partial Person{name=\" + name + \", age=\" + age + \"}\";",
        "    }",
        "  }",
        "}");
  }

  @Test
  public void twoRequiredProperties() {
    QualifiedName person = QualifiedName.of("com.example", "Person");
    QualifiedName generatedBuilder = QualifiedName.of("com.example", "Person_Builder");

    Datatype datatype = new Datatype.Builder()
        .setBuilder(person.nestedType("Builder").withParameters())
        .setExtensible(true)
        .setBuilderFactory(BuilderFactory.NO_ARGS_CONSTRUCTOR)
        .setBuilderSerializable(false)
        .setGeneratedBuilder(generatedBuilder.withParameters())
        .setHasToBuilderMethod(true)
        .setInterfaceType(false)
        .setPartialType(generatedBuilder.nestedType("Partial").withParameters())
        .setPropertyEnum(generatedBuilder.nestedType("Property").withParameters())
        .setType(person.withParameters())
        .setValueType(generatedBuilder.nestedType("Value").withParameters())
        .build();
    Property name = new Property.Builder()
        .setAllCapsName("NAME")
        .setBoxedType(STRING)
        .setCapitalizedName("Name")
        .setFullyCheckedCast(true)
        .setGetterName("name")
        .setName("name")
        .setType(STRING)
        .setUsingBeanConvention(false)
        .build();
    Property age = new Property.Builder()
        .setAllCapsName("AGE")
        .setBoxedType(INTEGER)
        .setCapitalizedName("Age")
        .setFullyCheckedCast(true)
        .setGetterName("age")
        .setName("age")
        .setType(INT)
        .setUsingBeanConvention(false)
        .build();
    GeneratedBuilder builder = new GeneratedBuilder(datatype, ImmutableMap.of(
        name, new DefaultProperty(datatype, name, false, unaryOperator(STRING)),
        age, new DefaultProperty(datatype, age, false, primitiveUnaryOperator(INT))));

    assertThat(builder).given(GuavaLibrary.AVAILABLE).generates(
        "// Autogenerated code. Do not modify.",
        "package com.example;",
        "",
        "import com.example.Person;",
        "import com.google.common.annotations.VisibleForTesting;",
        "import com.google.common.base.Preconditions;",
        "import java.util.EnumSet;",
        "import java.util.Objects;",
        "import java.util.function.IntUnaryOperator;",
        "import java.util.function.UnaryOperator;",
        "",
        "/** Auto-generated superclass of {@link Person.Builder}, "
            + "derived from the API of {@link Person}. */",
        "abstract class Person_Builder {",
        "",
        "  /**",
        "   * Creates a new builder using {@code value} as a template.",
        "   *",
        "   * <p>If {@code value} is a partial, the builder will return more partials.",
        "   */",
        "  public static Person.Builder from(Person value) {",
        "    return value.toBuilder();",
        "  }",
        "",
        "  private enum Property {",
        "    NAME(\"name\"),",
        "    AGE(\"age\"),",
        "    ;",
        "",
        "    private final String name;",
        "",
        "    private Property(String name) {",
        "      this.name = name;",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      return name;",
        "    }",
        "  }",
        "",
        "  private String name;",
        "  private int age;",
        "  private final EnumSet<Property> _unsetProperties = EnumSet.allOf(Property.class);",
        "",
        "  /**",
        "   * Sets the value to be returned by {@link Person#name()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code name} is null",
        "   */",
        "  public Person.Builder name(String name) {",
        "    this.name = Objects.requireNonNull(name);",
        "    _unsetProperties.remove(Property.NAME);",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Replaces the value to be returned by {@link Person#name()} by applying "
            + "{@code mapper} to it and",
        "   * using the result.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code mapper} is null or returns null",
        "   * @throws IllegalStateException if the field has not been set",
        "   */",
        "  public Person.Builder mapName(UnaryOperator<String> mapper) {",
        "    Objects.requireNonNull(mapper);",
        "    return name(mapper.apply(name()));",
        "  }",
        "",
        "  /**",
        "   * Returns the value that will be returned by {@link Person#name()}.",
        "   *",
        "   * @throws IllegalStateException if the field has not been set",
        "   */",
        "  public String name() {",
        "    Preconditions.checkState(!_unsetProperties.contains(Property.NAME), "
            + "\"name not set\");",
        "    return name;",
        "  }",
        "",
        "  /**",
        "   * Sets the value to be returned by {@link Person#age()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder age(int age) {",
        "    this.age = age;",
        "    _unsetProperties.remove(Property.AGE);",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Replaces the value to be returned by {@link Person#age()} by applying {@code mapper} "
            + "to it and",
        "   * using the result.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code mapper} is null",
        "   * @throws IllegalStateException if the field has not been set",
        "   */",
        "  public Person.Builder mapAge(IntUnaryOperator mapper) {",
        "    Objects.requireNonNull(mapper);",
        "    return age(mapper.applyAsInt(age()));",
        "  }",
        "",
        "  /**",
        "   * Returns the value that will be returned by {@link Person#age()}.",
        "   *",
        "   * @throws IllegalStateException if the field has not been set",
        "   */",
        "  public int age() {",
        "    Preconditions.checkState(!_unsetProperties.contains(Property.AGE), \"age not set\");",
        "    return age;",
        "  }",
        "",
        "  /**",
        "   * Copies values from {@code value}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder mergeFrom(Person value) {",
        "    Person_Builder defaults = new Person.Builder();",
        "    if (defaults._unsetProperties.contains(Property.NAME)",
        "        || !Objects.equals(value.name(), defaults.name())) {",
        "      name(value.name());",
        "    }",
        "    if (defaults._unsetProperties.contains(Property.AGE)"
            + " || value.age() != defaults.age()) {",
        "      age(value.age());",
        "    }",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Copies values from {@code template}, skipping unset properties.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder mergeFrom(Person.Builder template) {",
        "    // Upcast to access private fields; otherwise, oddly, we get an access violation.",
        "    Person_Builder base = template;",
        "    Person_Builder defaults = new Person.Builder();",
        "    if (!base._unsetProperties.contains(Property.NAME)",
        "        && (defaults._unsetProperties.contains(Property.NAME)",
        "            || !Objects.equals(template.name(), defaults.name()))) {",
        "      name(template.name());",
        "    }",
        "    if (!base._unsetProperties.contains(Property.AGE)",
        "        && (defaults._unsetProperties.contains(Property.AGE)"
            + " || template.age() != defaults.age())) {",
        "      age(template.age());",
        "    }",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Resets the state of this builder.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder clear() {",
        "    Person_Builder defaults = new Person.Builder();",
        "    name = defaults.name;",
        "    age = defaults.age;",
        "    _unsetProperties.clear();",
        "    _unsetProperties.addAll(defaults._unsetProperties);",
        "    return (Person.Builder) this;",
        "  }",
        "",
        "  /**",
        "   * Returns a newly-created {@link Person} based on the contents of this "
            + "{@code Builder}.",
        "   *",
        "   * @throws IllegalStateException if any field has not been set",
        "   */",
        "  public Person build() {",
        "    Preconditions.checkState(_unsetProperties.isEmpty(),"
            + " \"Not set: %s\", _unsetProperties);",
        "    return new Value(this);",
        "  }",
        "",
        "  /**",
        "   * Returns a newly-created partial {@link Person} for use in unit tests. "
            + "State checking will not",
        "   * be performed. Unset properties will throw an {@link UnsupportedOperationException} "
            + "when",
        "   * accessed via the partial object.",
        "   *",
        "   * <p>The builder returned by {@link Person.Builder#from(Person)} or "
            + "{@link Person#toBuilder()}",
        "   * will propagate the partial status of its input, overriding "
            + "{@link Person.Builder#build()",
        "   * build()} to return another partial. This allows for robust tests of modify-rebuild "
            + "code.",
        "   *",
        "   * <p>Partials should only ever be used in tests. "
            + "They permit writing robust test cases that won't",
        "   * fail if this type gains more application-level constraints "
            + "(e.g. new required fields) in",
        "   * future. If you require partially complete values in production code, "
            + "consider using a Builder.",
        "   */",
        "  @VisibleForTesting()",
        "  public Person buildPartial() {",
        "    return new Partial(this);",
        "  }",
        "",
        "  private static final class Value extends Person {",
        "    private final String name;",
        "    private final int age;",
        "",
        "    private Value(Person_Builder builder) {",
        "      this.name = builder.name;",
        "      this.age = builder.age;",
        "    }",
        "",
        "    @Override",
        "    public String name() {",
        "      return name;",
        "    }",
        "",
        "    @Override",
        "    public int age() {",
        "      return age;",
        "    }",
        "",
        "    @Override",
        "    public Person.Builder toBuilder() {",
        "      Person_Builder builder = new Person.Builder();",
        "      builder.name = name;",
        "      builder.age = age;",
        "      builder._unsetProperties.clear();",
        "      return (Person.Builder) builder;",
        "    }",
        "",
        "    @Override",
        "    public boolean equals(Object obj) {",
        "      if (!(obj instanceof Value)) {",
        "        return false;",
        "      }",
        "      Value other = (Value) obj;",
        "      return Objects.equals(name, other.name) && age == other.age;",
        "    }",
        "",
        "    @Override",
        "    public int hashCode() {",
        "      return Objects.hash(name, age);",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      return \"Person{name=\" + name + \", age=\" + age + \"}\";",
        "    }",
        "  }",
        "",
        "  private static final class Partial extends Person {",
        "    private final String name;",
        "    private final int age;",
        "    private final EnumSet<Property> _unsetProperties;",
        "",
        "    Partial(Person_Builder builder) {",
        "      this.name = builder.name;",
        "      this.age = builder.age;",
        "      this._unsetProperties = builder._unsetProperties.clone();",
        "    }",
        "",
        "    @Override",
        "    public String name() {",
        "      if (_unsetProperties.contains(Property.NAME)) {",
        "        throw new UnsupportedOperationException(\"name not set\");",
        "      }",
        "      return name;",
        "    }",
        "",
        "    @Override",
        "    public int age() {",
        "      if (_unsetProperties.contains(Property.AGE)) {",
        "        throw new UnsupportedOperationException(\"age not set\");",
        "      }",
        "      return age;",
        "    }",
        "",
        "    private static class PartialBuilder extends Person.Builder {",
        "      @Override",
        "      public Person build() {",
        "        return buildPartial();",
        "      }",
        "    }",
        "",
        "    @Override",
        "    public Person.Builder toBuilder() {",
        "      Person_Builder builder = new PartialBuilder();",
        "      builder.name = name;",
        "      builder.age = age;",
        "      builder._unsetProperties.clear();",
        "      builder._unsetProperties.addAll(_unsetProperties);",
        "      return (Person.Builder) builder;",
        "    }",
        "",
        "    @Override",
        "    public boolean equals(Object obj) {",
        "      if (!(obj instanceof Partial)) {",
        "        return false;",
        "      }",
        "      Partial other = (Partial) obj;",
        "      return Objects.equals(name, other.name)",
        "          && age == other.age",
        "          && Objects.equals(_unsetProperties, other._unsetProperties);",
        "    }",
        "",
        "    @Override",
        "    public int hashCode() {",
        "      return Objects.hash(name, age, _unsetProperties);",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      StringBuilder result = new StringBuilder(\"partial Person{\");",
        "      String separator = \"\";",
        "      if (!_unsetProperties.contains(Property.NAME)) {",
        "        result.append(\"name=\").append(name);",
        "        separator = \", \";",
        "      }",
        "      if (!_unsetProperties.contains(Property.AGE)) {",
        "        result.append(separator).append(\"age=\").append(age);",
        "      }",
        "      return result.append(\"}\").toString();",
        "    }",
        "  }",
        "}");
  }

  @Test
  public void genericType() {
    QualifiedName person = QualifiedName.of("com.example", "Person");
    QualifiedName generatedBuilder = QualifiedName.of("com.example", "Person_Builder");
    TypeParameterElementImpl paramA = newTypeParameterElement("A");
    TypeParameterElementImpl paramB = newTypeParameterElement("B");

    Datatype datatype = new Datatype.Builder()
        .setBuilder(person.nestedType("Builder").withParameters(paramA.asType(), paramB.asType()))
        .setExtensible(true)
        .setBuilderFactory(BuilderFactory.NO_ARGS_CONSTRUCTOR)
        .setBuilderSerializable(false)
        .setGeneratedBuilder(generatedBuilder.withParameters(paramA, paramB))
        .setHasToBuilderMethod(true)
        .setInterfaceType(false)
        .setPartialType(generatedBuilder.nestedType("Partial").withParameters(paramA, paramB))
        .setPropertyEnum(generatedBuilder.nestedType("Property").withParameters())
        .setType(person.withParameters(paramA, paramB))
        .setValueType(generatedBuilder.nestedType("Value").withParameters(paramA, paramB))
        .build();
    Property name = new Property.Builder()
        .setAllCapsName("NAME")
        .setCapitalizedName("Name")
        .setFullyCheckedCast(true)
        .setGetterName("name")
        .setName("name")
        .setType(paramA.asType())
        .setUsingBeanConvention(false)
        .build();
    Property age = new Property.Builder()
        .setAllCapsName("AGE")
        .setCapitalizedName("Age")
        .setFullyCheckedCast(true)
        .setGetterName("age")
        .setName("age")
        .setType(paramB.asType())
        .setUsingBeanConvention(false)
        .build();
    GeneratedBuilder builder = new GeneratedBuilder(datatype, ImmutableMap.of(
        name, new DefaultProperty(datatype, name, false, unaryOperator(paramA.asType())),
        age, new DefaultProperty(datatype, age, false, unaryOperator(paramB.asType()))));

    assertThat(builder).given(GuavaLibrary.AVAILABLE).generates(
        "// Autogenerated code. Do not modify.",
        "package com.example;",
        "",
        "import com.example.Person;",
        "import com.google.common.annotations.VisibleForTesting;",
        "import com.google.common.base.Preconditions;",
        "import java.util.EnumSet;",
        "import java.util.Objects;",
        "import java.util.function.UnaryOperator;",
        "",
        "/** Auto-generated superclass of {@link Person.Builder}, "
            + "derived from the API of {@link Person}. */",
        "abstract class Person_Builder<A, B> {",
        "",
        "  /**",
        "   * Creates a new builder using {@code value} as a template.",
        "   *",
        "   * <p>If {@code value} is a partial, the builder will return more partials.",
        "   */",
        "  public static <A, B> Person.Builder<A, B> from(Person<A, B> value) {",
        "    return value.toBuilder();",
        "  }",
        "",
        "  private enum Property {",
        "    NAME(\"name\"),",
        "    AGE(\"age\"),",
        "    ;",
        "",
        "    private final String name;",
        "",
        "    private Property(String name) {",
        "      this.name = name;",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      return name;",
        "    }",
        "  }",
        "",
        "  private A name;",
        "  private B age;",
        "  private final EnumSet<Property> _unsetProperties = EnumSet.allOf(Property.class);",
        "",
        "  /**",
        "   * Sets the value to be returned by {@link Person#name()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code name} is null",
        "   */",
        "  public Person.Builder<A, B> name(A name) {",
        "    this.name = Objects.requireNonNull(name);",
        "    _unsetProperties.remove(Property.NAME);",
        "    return (Person.Builder<A, B>) this;",
        "  }",
        "",
        "  /**",
        "   * Replaces the value to be returned by {@link Person#name()} "
            + "by applying {@code mapper} to it and",
        "   * using the result.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code mapper} is null or returns null",
        "   * @throws IllegalStateException if the field has not been set",
        "   */",
        "  public Person.Builder<A, B> mapName(UnaryOperator<A> mapper) {",
        "    Objects.requireNonNull(mapper);",
        "    return name(mapper.apply(name()));",
        "  }",
        "",
        "  /**",
        "   * Returns the value that will be returned by {@link Person#name()}.",
        "   *",
        "   * @throws IllegalStateException if the field has not been set",
        "   */",
        "  public A name() {",
        "    Preconditions.checkState(!_unsetProperties.contains(Property.NAME), "
            + "\"name not set\");",
        "    return name;",
        "  }",
        "",
        "  /**",
        "   * Sets the value to be returned by {@link Person#age()}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code age} is null",
        "   */",
        "  public Person.Builder<A, B> age(B age) {",
        "    this.age = Objects.requireNonNull(age);",
        "    _unsetProperties.remove(Property.AGE);",
        "    return (Person.Builder<A, B>) this;",
        "  }",
        "",
        "  /**",
        "   * Replaces the value to be returned by {@link Person#age()} "
            + "by applying {@code mapper} to it and",
        "   * using the result.",
        "   *",
        "   * @return this {@code Builder} object",
        "   * @throws NullPointerException if {@code mapper} is null or returns null",
        "   * @throws IllegalStateException if the field has not been set",
        "   */",
        "  public Person.Builder<A, B> mapAge(UnaryOperator<B> mapper) {",
        "    Objects.requireNonNull(mapper);",
        "    return age(mapper.apply(age()));",
        "  }",
        "",
        "  /**",
        "   * Returns the value that will be returned by {@link Person#age()}.",
        "   *",
        "   * @throws IllegalStateException if the field has not been set",
        "   */",
        "  public B age() {",
        "    Preconditions.checkState(!_unsetProperties.contains(Property.AGE), \"age not set\");",
        "    return age;",
        "  }",
        "",
        "  /**",
        "   * Copies values from {@code value}.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder<A, B> mergeFrom(Person<A, B> value) {",
        "    Person_Builder<A, B> defaults = new Person.Builder<>();",
        "    if (defaults._unsetProperties.contains(Property.NAME)",
        "        || !Objects.equals(value.name(), defaults.name())) {",
        "      name(value.name());",
        "    }",
        "    if (defaults._unsetProperties.contains(Property.AGE)",
        "        || !Objects.equals(value.age(), defaults.age())) {",
        "      age(value.age());",
        "    }",
        "    return (Person.Builder<A, B>) this;",
        "  }",
        "",
        "  /**",
        "   * Copies values from {@code template}, skipping unset properties.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder<A, B> mergeFrom(Person.Builder<A, B> template) {",
        "    // Upcast to access private fields; otherwise, oddly, we get an access violation.",
        "    Person_Builder<A, B> base = template;",
        "    Person_Builder<A, B> defaults = new Person.Builder<>();",
        "    if (!base._unsetProperties.contains(Property.NAME)",
        "        && (defaults._unsetProperties.contains(Property.NAME)",
        "            || !Objects.equals(template.name(), defaults.name()))) {",
        "      name(template.name());",
        "    }",
        "    if (!base._unsetProperties.contains(Property.AGE)",
        "        && (defaults._unsetProperties.contains(Property.AGE)",
        "            || !Objects.equals(template.age(), defaults.age()))) {",
        "      age(template.age());",
        "    }",
        "    return (Person.Builder<A, B>) this;",
        "  }",
        "",
        "  /**",
        "   * Resets the state of this builder.",
        "   *",
        "   * @return this {@code Builder} object",
        "   */",
        "  public Person.Builder<A, B> clear() {",
        "    Person_Builder<A, B> defaults = new Person.Builder<>();",
        "    name = defaults.name;",
        "    age = defaults.age;",
        "    _unsetProperties.clear();",
        "    _unsetProperties.addAll(defaults._unsetProperties);",
        "    return (Person.Builder<A, B>) this;",
        "  }",
        "",
        "  /**",
        "   * Returns a newly-created {@link Person} based on the contents of this "
            + "{@code Builder}.",
        "   *",
        "   * @throws IllegalStateException if any field has not been set",
        "   */",
        "  public Person<A, B> build() {",
        "    Preconditions.checkState(_unsetProperties.isEmpty(),"
            + " \"Not set: %s\", _unsetProperties);",
        "    return new Value<>(this);",
        "  }",
        "",
        "  /**",
        "   * Returns a newly-created partial {@link Person} for use in unit tests. "
            + "State checking will not",
        "   * be performed. Unset properties will throw an {@link UnsupportedOperationException} "
            + "when",
        "   * accessed via the partial object.",
        "   *",
        "   * <p>The builder returned by {@link Person.Builder#from(Person)} or "
            + "{@link Person#toBuilder()}",
        "   * will propagate the partial status of its input, overriding "
            + "{@link Person.Builder#build()",
        "   * build()} to return another partial. This allows for robust tests of modify-rebuild "
            + "code.",
        "   *",
        "   * <p>Partials should only ever be used in tests. "
            + "They permit writing robust test cases that won't",
        "   * fail if this type gains more application-level constraints "
            + "(e.g. new required fields) in",
        "   * future. If you require partially complete values in production code, "
            + "consider using a Builder.",
        "   */",
        "  @VisibleForTesting()",
        "  public Person<A, B> buildPartial() {",
        "    return new Partial<>(this);",
        "  }",
        "",
        "  private static final class Value<A, B> extends Person<A, B> {",
        "    private final A name;",
        "    private final B age;",
        "",
        "    private Value(Person_Builder<A, B> builder) {",
        "      this.name = builder.name;",
        "      this.age = builder.age;",
        "    }",
        "",
        "    @Override",
        "    public A name() {",
        "      return name;",
        "    }",
        "",
        "    @Override",
        "    public B age() {",
        "      return age;",
        "    }",
        "",
        "    @Override",
        "    public Person.Builder<A, B> toBuilder() {",
        "      Person_Builder<A, B> builder = new Person.Builder<>();",
        "      builder.name = name;",
        "      builder.age = age;",
        "      builder._unsetProperties.clear();",
        "      return (Person.Builder<A, B>) builder;",
        "    }",
        "",
        "    @Override",
        "    public boolean equals(Object obj) {",
        "      if (!(obj instanceof Value)) {",
        "        return false;",
        "      }",
        "      Value<?, ?> other = (Value<?, ?>) obj;",
        "      return Objects.equals(name, other.name) && Objects.equals(age, other.age);",
        "    }",
        "",
        "    @Override",
        "    public int hashCode() {",
        "      return Objects.hash(name, age);",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      return \"Person{name=\" + name + \", age=\" + age + \"}\";",
        "    }",
        "  }",
        "",
        "  private static final class Partial<A, B> extends Person<A, B> {",
        "    private final A name;",
        "    private final B age;",
        "    private final EnumSet<Property> _unsetProperties;",
        "",
        "    Partial(Person_Builder<A, B> builder) {",
        "      this.name = builder.name;",
        "      this.age = builder.age;",
        "      this._unsetProperties = builder._unsetProperties.clone();",
        "    }",
        "",
        "    @Override",
        "    public A name() {",
        "      if (_unsetProperties.contains(Property.NAME)) {",
        "        throw new UnsupportedOperationException(\"name not set\");",
        "      }",
        "      return name;",
        "    }",
        "",
        "    @Override",
        "    public B age() {",
        "      if (_unsetProperties.contains(Property.AGE)) {",
        "        throw new UnsupportedOperationException(\"age not set\");",
        "      }",
        "      return age;",
        "    }",
        "",
        "    private static class PartialBuilder<A, B> extends Person.Builder<A, B> {",
        "      @Override",
        "      public Person<A, B> build() {",
        "        return buildPartial();",
        "      }",
        "    }",
        "",
        "    @Override",
        "    public Person.Builder<A, B> toBuilder() {",
        "      Person_Builder<A, B> builder = new PartialBuilder<>();",
        "      builder.name = name;",
        "      builder.age = age;",
        "      builder._unsetProperties.clear();",
        "      builder._unsetProperties.addAll(_unsetProperties);",
        "      return (Person.Builder<A, B>) builder;",
        "    }",
        "",
        "    @Override",
        "    public boolean equals(Object obj) {",
        "      if (!(obj instanceof Partial)) {",
        "        return false;",
        "      }",
        "      Partial<?, ?> other = (Partial<?, ?>) obj;",
        "      return Objects.equals(name, other.name)",
        "          && Objects.equals(age, other.age)",
        "          && Objects.equals(_unsetProperties, other._unsetProperties);",
        "    }",
        "",
        "    @Override",
        "    public int hashCode() {",
        "      return Objects.hash(name, age, _unsetProperties);",
        "    }",
        "",
        "    @Override",
        "    public String toString() {",
        "      StringBuilder result = new StringBuilder(\"partial Person{\");",
        "      String separator = \"\";",
        "      if (!_unsetProperties.contains(Property.NAME)) {",
        "        result.append(\"name=\").append(name);",
        "        separator = \", \";",
        "      }",
        "      if (!_unsetProperties.contains(Property.AGE)) {",
        "        result.append(separator).append(\"age=\").append(age);",
        "      }",
        "      return result.append(\"}\").toString();",
        "    }",
        "  }",
        "}");
  }
}
